import numpy as np 
# class LR():
#     def __init__(self,num_epochs=100,lr=0.1):
#         self.num_epochs=num_epochs
#         self.lr=lr
#         self.W = None 
#         self.b = None
    
#     def sigmoid(self,x):
#         return 1/(1+np.exp(-x))
    
#     def fit(self,X,y):
#         m,n=X.shape
#         self.W = np.random.uniform(0,1,size=n)
#         self.b = 0 
#         for _ in range(self.num_epochs):
#             z = np.dot(X,self.W) + self.b 
#             y_pred = self.sigmoid(z)
#             dw = np.dot(X.T,(y_pred - y))/m 
#             db = np.sum(y_pred - y)/m 
#             self.W -= self.lr*dw 
#             self.b -= self.lr*db 

#     def predict(self,X):
#         if self.W is None:
#             print('the model is not fitted yet')
#             return None 
#         return self.sigmoid(np.dot(X,self.W) + self.b)


# class DNN():
#     def __init__(self,h1=16,h2=8,h3=4,num_epochs=100,lr=0.1):
#         self.h1=h1 
#         self.h2=h2
#         self.h3=h3
#         self.num_epochs=num_epochs s
#         self.lr=lr 
#         self.is_fitted=False 

#     def sigmoid(self,x):
#         return 1/(1+np.exp(-x))

#     def fit(self,X,y):
#         m,n=X.shape
#         self.W1=np.random.uniform(0,1,size=(n,self.h1))
#         self.W2=np.random.uniform(0,1,size=(self.h1,self.h2))
#         self.W3=np.random.uniform(0,1,size=(self.h2,1))
#         self.b1, self.b2, self.b3 = 0,0,0 
#         for _ in range(self.num_epochs):
#             z1=np.dot(X,self.W1)+self.b1 
#             o1=self.sigmoid(z1)
#             z2=np.dot(o1,self.W2)+self.b2 
#             o2=self.sigmoid(z2)
#             z3=np.dot(o2,self.W3)+self.b3
#             y_pred=self.sigmoid(z3)

#             dz3 = (y_pred - y.reshape(-1,1)) 
#             dz2 = np.dot(dz3, self.W3.T)*o2*(1-o2)
#             dz1 = np.dot(dz2, self.W2.T)*o1*(1-o1)

#             dW3 = np.dot(o2.T,dz3)/m 
#             db3 = np.sum(dz3)/m
#             dW2 = np.dot(o1.T,dz2)/m 
#             db2 = np.sum(dz2)/m
#             dW1 = np.dot(X.T,dz1)/m 
#             db1 = np.sum(dz1)/m

#             self.W1 -= self.lr*dW1
#             self.b1 -= self.lr*db1
#             self.W2 -= self.lr*dW2
#             self.b2 -= self.lr*db2
#             self.W3 -= self.lr*dW3
#             self.b3 -= self.lr*db3
#         self.is_fitted=True 

#     def predict(self,X):
#         if not self.is_fitted: return None 
#         z1=np.dot(X,self.W1)+self.b1 
#         o1=self.sigmoid(z1)
#         z2=np.dot(o1,self.W2)+self.b2 
#         o2=self.sigmoid(z2)
#         z3=np.dot(o2,self.W3)+self.b3
#         y_pred=self.sigmoid(z3)
#         return y_pred.squeeze()
    

class LR:
    def __init__(self,lr=0.01,epochs=1000):
        self.lr = lr 
        self.epochs = epochs 

    def sigmoid(self,x):
        return 1/(1+np.exp(-x))

    def fit(self,X,y):
        m,n = X.shape
        self.W = np.random.uniform(size=(n,1))
        self.b = 0
        for _ in range(self.epochs): 
            y_pred = self.sigmoid(np.dot(X,self.W)+self.b)
            dW = np.dot(X.T,(y_pred - y.reshape(m,1)))/m 
            db = np.sum(y_pred-y)/m 
            self.W -= self.lr*dW 
            self.b -= self.lr*db 

    def predict(self,X):
        return self.sigmoid(np.dot(X,self.W)+self.b)

          
class DNN:
    def __init__(self,lr=0.01,epochs=1000,h1=4,h2=3):
        self.lr = lr 
        self.epochs = epochs 
        self.h1 = h1 
        self.h2 = h2 
    
    def sigmoid(self,x):
        return 1/(1+np.exp(-x))
    
    def fit(self,X,y):
        m,n = X.shape
        self.W1 = np.random.uniform(size=(n,self.h1))
        self.W2 = np.random.uniform(size=(self.h1,self.h2))
        self.W3 = np.random.uniform(size=(self.h2,1))
        self.b1,self.b2,self.b3 = 0,0,0
        for _ in range(self.epochs):
            z1 = np.dot(X,self.W1)+self.b1 
            o1 = self.sigmoid(z1)
            z2 = np.dot(o1,self.W2)+self.b2 
            o2 = self.sigmoid(z2)
            z3 = np.dot(o2,self.W3)+self.b3 
            y_pred = self.sigmoid(z3)

            dz3 = y_pred - y.reshape(m,1) #(m,1)
            dz2 = np.dot(dz3,self.W3.T)*o2*(1-o2) #(m,h2)
            dz1 = np.dot(dz2,self.W2.T)*o1*(1-o1) 
            dW3 = np.dot(o2.T,dz3)/m 
            db3 = np.sum(dz3)/m
            dW2 = np.dot(o1.T,dz2)/m 
            db2 = np.sum(dz2)/m
            dW1 = np.dot(X.T,dz1)/m 
            db1 = np.sum(dz1)/m
            self.W1 -= self.lr*dW1 
            self.b1 -= self.lr*db1
            self.W2 -= self.lr*dW2 
            self.b2 -= self.lr*db2
            self.W3 -= self.lr*dW3 
            self.b3 -= self.lr*db3

    def predict(self,X):
        z1 = np.dot(X,self.W1)+self.b1 
        o1 = self.sigmoid(z1)
        z2 = np.dot(o1,self.W2)+self.b2 
        o2 = self.sigmoid(z2)
        z3 = np.dot(o2,self.W3)+self.b3 
        y_pred = self.sigmoid(z3)
        return y_pred



if __name__=="__main__":
    X=np.random.uniform(size=(5,2))
    y=np.array([0,0,0,1,1])
    model=LR()
    model.fit(X,y)
    out = model.predict(X)
    print(out)

    model=DNN()
    model.fit(X,y)
    out = model.predict(X)
    print(out)






